16.5 释放

在运行时入口函数main.main里,会专门启动一个监控任务sysmon,它每隔一段时间就会检查heap里的闲置内存块。

proc.go

func sysmon() { scavengelimit:=int64(5601e9)

for{ usleep(delay)

if lastscavenge+scavengelimit/2<now{ 
    mHeap_Scavenge(int32(nscavenge),uint64(now),uint64(scavengelimit)) 
    lastscavenge=now
 } 

} }

遍历free、freelarge里的所有span,如闲置时间超过阈值,则释放其关联的物理内存。

mheap.go

func mHeap_Scavenge(k int32,now,limit uint64) { h:= &mheap_

// 遍历free数组里的所有链表 for i:=0;i<len(h.free);i++ { sumreleased+=scavengelist(&h.free[i],now,limit) }

// 遍历freelarge链表 sumreleased+=scavengelist(&h.freelarge,now,limit) }

func scavengelist(list*mspan,now,limit uint64)uintptr{ var sumreleased uintptr

// 遍历链表 for s:=list.next;s!=list;s=s.next{ // 检查闲置时间是否超出限制,而且内存没有全部被释放过 // 因为存在span合并的情况,所以有局部释放很正常 if(now-uint64(s.unusedsince)) >limit&&s.npreleased!=s.npages{ // 更新释放计数属性 released:= (s.npages-s.npreleased) << _PageShift sumreleased+=released s.npreleased=s.npages

     // 释放内存 
    sysUnused((unsafe.Pointer)(s.start<<_PageShift),s.npages<<_PageShift) 
 } 

} return sumreleased }

所谓物理内存释放,另有玄虚。

mem_linux.go

func sysUnused(v unsafe.Pointer,n uintptr) { madvise(v,n, _MADV_DONTNEED) }

系统调用madvise告知操作系统某段内存暂不使用,建议内核收回对应物理内存。当然,这只是一个建议,是否回收由内核决定。如物理内存资源充足,该建议可能会被忽略,以避免无谓的损耗。而当再次使用该内存块时,会引发缺页异常,内核会自动重新关联物理内存页。

分配器面对的是虚拟内存,所以在地址空间充足的情况下,根本无须放弃这段虚拟内存,无须收回mspan等管理对象,这也是arena能线性扩张的根本原因。

Microsoft Windows并不支持类似madvise的机制,须在获取span时主动补上被VirtualFree掉的内存。

mem_windows.go

func sysUnused(v unsafe.Pointer,n uintptr) { r:=stdcall3(_VirtualFree,uintptr(v),n, _MEM_DECOMMIT) }

func sysUsed(v unsafe.Pointer,n uintptr) { r:=stdcall4(_VirtualAlloc,uintptr(v),n, _MEM_COMMIT, _PAGE_READWRITE) }

mheap.go

func mHeap_AllocSpanLocked(h*mheap,npage uintptr) *mspan{ …

HaveSpan: // 如果被释放过物理内存,重新补上 if s.npreleased>0{ sysUsed((unsafe.Pointer)(s.start<<_PageShift),s.npages<<_PageShift) s.npreleased=0 }

return s }

多数UNIX-Like系统都支持madvise,所以它们的sysUsed函数大多什么都不做。

除周期性自动处理外,也可以调用runtime/debug.FreeOSMemory函数主动释放。